package com.google.jessewilson.codesearch.ui.tree;

import com.google.jessewilson.codesearch.api.Line;
import com.google.jessewilson.codesearch.api.ProgrammingLanguage;
import com.google.jessewilson.codesearch.api.QueryResponse;
import com.google.jessewilson.codesearch.api.ResultsPopulator;
import com.google.jessewilson.codesearch.ui.ResultActionsProvider;
import com.google.jessewilson.codesearch.ui.TableModelDecorator;
import com.google.jessewilson.codesearch.util.MoreCollections;

import com.intellij.util.ui.UIUtil;

import java.awt.*;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.NoSuchElementException;

import javax.swing.*;
import javax.swing.table.DefaultTableModel;

import ca.odell.glazedlists.FunctionList;
import ca.odell.glazedlists.GlazedLists;
import ca.odell.glazedlists.TransformedList;
import ca.odell.glazedlists.TreeList;
import ca.odell.glazedlists.gui.TableFormat;
import ca.odell.glazedlists.swing.EventTableModel;
import ca.odell.glazedlists.swing.GlazedListsSwing;
import ca.odell.glazedlists.swing.TreeTableSupport;

/**
 * Display and navigate Code Search results.
 *
 * @author jessewilson
 */
public class ResultsForOneQuery {

  private static final DefaultTableModel EMPTY_TABLE_MODEL = new DefaultTableModel(0, 0);

  private final QueryResponse results;

  private final JPanel resultsPane;
  private final FileListenerSupport fileListenerSupport;

  private final TransformedList<Line,Line> linesOnSwingThread;
  private final FunctionList<Line,Element> linesOfCode;
  private final EventTableModel<Element> tableModel;
  private final TreeTableSupport treeTableSupport;

  private final TreeList<Element> treeList;
  private final JTable table;

  public ResultsForOneQuery(QueryResponse results) {
    this.results = results;

    // proxy the EventList onto the appropriate thread
    linesOnSwingThread = GlazedListsSwing.swingThreadProxyList(results.getLines());

    // construct the treetable model
    linesOfCode = new FunctionList<Line, Element>(linesOnSwingThread, new LineToElementModel());
    treeList = new TreeList<Element>(linesOfCode,
        new ElementTreeFormat(), TreeList.NODES_START_EXPANDED,
        (Comparator) GlazedLists.comparableComparator());
    TableFormat<Element> tableFormat = new ElementTableFormat();
    tableModel = new EventTableModel<Element>(treeList, tableFormat);

    // install a decorator to load more rows when they're available
    FetchMoreRowsWhenNecessaryTableModel fetchMoreRowsWhenNecessaryTableModel
        = new FetchMoreRowsWhenNecessaryTableModel();

    // construct the treetable components
    table = new JTable(fetchMoreRowsWhenNecessaryTableModel);
    table.getColumnModel().getColumn(0).setCellRenderer(new ElementRenderer());
    table.getSelectionModel().setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
    table.setTableHeader(null);
    treeTableSupport = TreeTableSupport.install(table, treeList, 0);

    // build the user interface
    JScrollPane scrollPane = new JScrollPane(table,
        JScrollPane.VERTICAL_SCROLLBAR_ALWAYS,
        JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);
    resultsPane = new JPanel(new BorderLayout());
    resultsPane.setBackground(UIUtil.getTreeTextBackground());
    resultsPane.add(scrollPane, BorderLayout.CENTER);

    // handle double clicks and key events on the lines
    this.fileListenerSupport = new FileListenerSupport(table, treeList, results);

//    table.getInputMap(JComponent.WHEN_FOCUSED)
//        .put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), "MY_ACTION");
//    table.getActionMap().put("MY_ACTION", new AbstractAction() {
//      public void actionPerformed(ActionEvent e) {
//      }
//    });

  }

  public void addActionsProvider(ResultActionsProvider actionsProvider) {
    this.fileListenerSupport.addActionsProvider(actionsProvider);
  }

  public void removeActionsProvider(ResultActionsProvider actionsProvider) {
    this.fileListenerSupport.removeActionsProvider(actionsProvider);
  }

  public JComponent getComponent() {
    return resultsPane;
  }

  public JTable getTable() {
    return table;
  }

  public TreeList<Element> getTreeList() {
    return treeList;
  }

  public void dispose() {
    treeTableSupport.uninstall();
    table.setModel(EMPTY_TABLE_MODEL);
    tableModel.dispose();
    treeList.dispose();
    linesOfCode.dispose();
    linesOnSwingThread.dispose();
  }

  private class ElementTableFormat implements TableFormat<Element> {

    public int getColumnCount() {
      return 1;
    }

    public String getColumnName(int i) {
      return "Title";
    }

    public Object getColumnValue(Element element, int i) {
      return element;
    }
  }

  private class ElementTreeFormat implements TreeList.Format<Element> {

    public void getPath(List<Element> elements, Element element) {
      elements.add(new Element(Type.LANGUAGE, element.getProgrammingLanguage(), null, null, null));
      if(element.getContentRoot() != null) {
        elements.add(new Element(Type.CONTENT_ROOT, element.getProgrammingLanguage(), element.getContentRoot(), null, null));
      }
      elements.add(new Element(Type.FILE, element.getProgrammingLanguage(), element.getContentRoot(), element.getFile(), null));
      elements.add(new Element(Type.LINE, element.getProgrammingLanguage(), element.getContentRoot(), element.getFile(), element.getLine()));
    }

    public boolean allowsChildren(Element element) {
      return element.getType() != Type.LINE;
    }
  }

  private class LineToElementModel implements FunctionList.Function<Line,Element> {
    public Element evaluate(Line line) {
      return new Element(Type.LINE,
          ProgrammingLanguage.fromFileName(line.getCodeFile().getPath().getFileName()),
          line.getCodeFile().getContentRoot(), line.getCodeFile(), line);
    }
  }

  /**
   * Handle mouse and key events on the selected line.
   */
  private static class FileListenerSupport implements MouseListener,
      KeyListener {

    private final JTable table;
    private final List<Element> elements;
    private final QueryResponse results;

    private final List<ResultActionsProvider> actionsProviders = new ArrayList<ResultActionsProvider>();

    public FileListenerSupport(JTable table, List<Element> elements, QueryResponse results) {
      this.table = table;
      this.elements = elements;
      this.results = results;

      table.addMouseListener(this);
      table.addKeyListener(this);
    }

    public void mousePressed(MouseEvent e) { }

    public void mouseEntered(MouseEvent e) { }

    public void mouseExited(MouseEvent e) { }

    public void mouseReleased(MouseEvent e) {
      Element selected = getSelectedAtPoint(e);
      if(selected == null) {
        return;
      }

      // show a popup menu of user-provided actions
      if (e.isPopupTrigger() || (e.getModifiers() & MouseEvent.BUTTON3_MASK) != 0) {
        // lazily build the popup menu
        JPopupMenu popupMenu = null;

        for (ResultActionsProvider actionsProvider : actionsProviders) {
          List<Action> actions = actionsProvider
              .actions(selected.getFile(), selected.getLine(), results);
          for (Action action : actions) {
            JMenuItem menuItem = new JMenuItem(action);

            if (popupMenu == null) {
              popupMenu = new JPopupMenu();
            }
            popupMenu.add(menuItem);
          }
        }

        if(popupMenu != null) {
          popupMenu.show(e.getComponent(), e.getX(), e.getY());
        }
      }
    }

    public void mouseClicked(MouseEvent e) {
      Element selected = getSelectedAtPoint(e);
      if(selected == null) {
        return;
      }

      // activate the default action on a double click
      if (e.getClickCount() == 2) {
        defaultAction(selected);
      }
    }

    private void defaultAction(Element selected) {
      for (ResultActionsProvider actionsProvider : actionsProviders) {
        actionsProvider.doDefaultAction(selected.getFile(), selected.getLine(), results);
      }
    }

    private Element getSelectedAtPoint(MouseEvent mouseEvent) {
      int row = table.rowAtPoint(mouseEvent.getPoint());
      if (row < 0 || row > elements.size()) {
        return null;
      }
      Element selected = elements.get(row);
      if (selected.getType()!= Type.FILE && selected.getType() != Type.LINE) {
        return null;
      }

      return selected;
    }


    /**
     * If the user hits "enter" on a line, run the default action.
     */
    public void keyTyped(KeyEvent e) {
      if (e.getKeyChar() == KeyEvent.VK_ENTER) {
        Element selected = getSelectedElement();
        if (selected == null) {
          return;
        }
        defaultAction(selected);
      }
    }

    private Element getSelectedElement() {
      int row = table.getSelectedRow();
      Element selected = elements.get(row);
      if (selected.getType()!= Type.FILE && selected.getType() != Type.LINE) {
        return null;
      }

      return selected;
    }

    public void keyPressed(KeyEvent e) { }

    public void keyReleased(KeyEvent e) { }

    public void addActionsProvider(ResultActionsProvider actionsProvider) {
      actionsProviders.add(actionsProvider);
    }

    public void removeActionsProvider(ResultActionsProvider actionsProvider) {
      boolean removed = MoreCollections.removeByIdentity(actionsProviders, actionsProvider);
      if(!removed) throw new NoSuchElementException();
    }
  }

  /**
   * Invokes a request for additional rows in the request when the last row is displayed.
   */
  private class FetchMoreRowsWhenNecessaryTableModel extends TableModelDecorator {
    public FetchMoreRowsWhenNecessaryTableModel() {
      super(ResultsForOneQuery.this.tableModel);
    }

    public Object getValueAt(int rowIndex, int columnIndex) {
      if (columnIndex == 0 && rowIndex == getRowCount() - 1) {
        SwingUtilities.invokeLater(new Runnable() {
          public void run() {
            ResultsPopulator resultsPopulator = ResultsForOneQuery.this.results.getResultsPopulator();
            if (resultsPopulator.getState() == ResultsPopulator.State.RESULTS_OUTSTANDING) {
              resultsPopulator.fetchMoreResults();
            }
          }
        });
      }

      return super.getValueAt(rowIndex, columnIndex);
    }
  }
}
